package hasthi.decision.planning;

import hasthi.actuators.ActionContext;
import hasthi.actuators.CreateServiceAction;
import hasthi.actuators.ManagementAction;
import hasthi.actuators.SystemConfig;
import hasthi.actuators.actions.ActionCallback;
import hasthi.common.ErrorManager;
import hasthi.common.HasthiException;
import hasthi.common.constants.HasthiConstants;
import hasthi.config.ManagementBehaviour;
import hasthi.config.ResourceProfile;
import hasthi.metamodel.resource.ManagedResource;
import hasthi.tools.FailureDetectionTools;
import hasthi.tools.OrderedGrpah;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;

import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SimplePlanner implements Planner {
    private static Log log = LogFactory.getLog(HasthiConstants.LOGGER_NAME);

    private Map<QName, List<ManagementAction>> createdServices = new ConcurrentHashMap<QName, List<ManagementAction>>();
    private Map<QName, List<ManagementAction>> servicesBeingCreated = new ConcurrentHashMap<QName, List<ManagementAction>>();
    private Map<QName, List<ActionCallback>> actionCallbackMap = new ConcurrentHashMap<QName, List<ActionCallback>>();

    private ActionContext actionContext;
    private OrderedGrpah<QName> dependencyGraph = new OrderedGrpah<QName>();

    // TODO may be there is a better representation of Jobs (e.g. With Cube)
    // private List<ManagementAction> queuedJobs = new
    // Vector<ManagementAction>();
    // private List<ManagementAction> activeJobs = new
    // ArrayList<ManagementAction>();
    // private List<ManagementAction> scheduledJobs = new
    // ArrayList<ManagementAction>();
    // private List<ManagementAction> failedJobs = new
    // ArrayList<ManagementAction>();
    // /**
    // * Fixed sized circuler buffer that keep last N Jobs
    // */
    // private List<ManagementAction> history = new
    // ArrayList<ManagementAction>();
    //    
    // private List<ManagementAction> tempList = new
    // ArrayList<ManagementAction>();
    //    
    //    
    // private Map<String,List<ManagementAction>> setPropertyActionMap = new
    // TreeMap<String, List<ManagementAction>>();

    public void init(ActionContext actionContext) {
        this.actionContext = actionContext;
        SystemConfig systemConfig = actionContext.getSystemConfig();
        ResourceProfile[] resourceConfigs = systemConfig.getProfile().getResourceArray();

        for (ResourceProfile resourceConfig : resourceConfigs) {
            ManagementBehaviour behaviour = resourceConfig.getBehaviour();
            if (behaviour != null && behaviour.getDependancyArray() != null) {
                for (QName name : behaviour.getDependancyArray()) {
                    QName baseName = resourceConfig.getName();
                    // following is to fix the namespaces assigned to
                    // non-qualified names
                    if ("http://extreme.indiana.edu/hasthi/config/".equals(baseName
                            .getNamespaceURI())) {
                        baseName = new QName(baseName.getLocalPart());
                    }
                    if ("http://extreme.indiana.edu/hasthi/config/".equals(name.getNamespaceURI())) {
                        name = new QName(name.getLocalPart());
                    }
                    dependencyGraph.addPartialOrder(baseName, name);
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see hasthi.decision.planning.Planner#planActions(java.util.List)
     */
    public List<ManagementAction> planActions(List<ManagementAction> actions)
            throws HasthiException {
        // TODO retry failed Jobs, but make sure to remove them after N retires
        // queuedJobs.addAll(actions);
        // remove actions older than 2 minutes
        synchronized (this) {
            Iterator<QName> keys = createdServices.keySet().iterator();
            while (keys.hasNext()) {
                QName name = keys.next();
                List<ManagementAction> storedActions = createdServices.get(name);
                Iterator<ManagementAction> actionsIt = storedActions.iterator();
                while (actionsIt.hasNext()) {
                    ManagementAction action = actionsIt.next();
                    if (System.currentTimeMillis() - action.getStartedTime() > 1000 * 60 * 2) {
                        actionsIt.remove();
                    }
                }
                // remove the array if all actions are empty
                if (storedActions.isEmpty()) {
                    keys.remove();
                }
            }
        }
        return actions;
        // //Compile the Planned Tasks
        // List<ManagementAction> palnnedJobs = new
        // ArrayList<ManagementAction>();
        // for(ManagementAction action:actions){
        // //This line makes sure no duplicate Job goes in while one is in
        // progress
        // if(activeJobs.contains(action.getActionID())){
        // log.info("Drop duplicate Job "+ action.getActionID());
        // continue;
        // }
        // palnnedJobs.add(action);
        // }
        //        
    }

    /*
     * (non-Javadoc)
     * 
     * @see hasthi.decision.planning.Planner#resolveConflicts(java.util.List)
     */
    /*
     * private List<ManagementAction> resolveConflicts(List<ManagementAction>
     * actions){ setPropertyActionMap.clear();
     * 
     * List<ManagementAction> actionsToRemove = new ArrayList<ManagementAction>();
     * 
     * /** Following code resolve the conflicts among set property actions where
     * both actions sets same property.
     *//*
         * //Find all Set Property actions List<SetPropertyAction>
         * setPropertyActions = new ArrayList<SetPropertyAction>();
         * for(ManagementAction action:actions){ if(action instanceof
         * SetPropertyAction){
         * setPropertyActions.add((SetPropertyAction)action); } } //Sort by ID,
         * effectively group the similer actions together
         * Collections.sort(setPropertyActions, new Comparator<SetPropertyAction>() {
         * public int compare(SetPropertyAction a1, SetPropertyAction a2) {
         * String id1 = createPropertyID(a1); String id2 = createPropertyID(a2);
         * return id1.compareTo(id2); } }); //If there are more than one action
         * with same ID, only leave the high priority action String lastID =
         * null; int lastPriority = -1; SetPropertyAction lastAction = null;
         * for(SetPropertyAction action:setPropertyActions){ String id =
         * createPropertyID(action); if(id.equals(lastID)){ if(lastPriority <
         * action.getPriorty()){ actionsToRemove.remove(lastAction); lastAction =
         * action; lastPriority = action.getPriorty(); }else{
         * actionsToRemove.remove(action); } }else{ lastID = id; lastAction =
         * action; lastPriority = action.getPriorty(); } }
         * actions.removeAll(actionsToRemove); return actions; }
         */
    public void actionSceduled(ManagementAction action) {
        if (action instanceof CreateServiceAction) {
            addToMap(servicesBeingCreated, (CreateServiceAction) action);
        }
    }

    public void actionActive(ManagementAction action) {
        // /queuedJobs.remove(action);
        // activeJobs.add(action);
    }

    public void actionCompleted(ManagementAction action) {
        if (action instanceof CreateServiceAction) {
            removeFromMap(servicesBeingCreated, (CreateServiceAction) action);
            addToMap(createdServices, (CreateServiceAction) action);

            List<ActionCallback> callbacks = actionCallbackMap.get(((CreateServiceAction) action)
                    .getServiceType());
            if (callbacks != null) {
                for (ActionCallback callback : callbacks) {
                    try {
                        callback.actionSucessful(action);
                    } catch (Throwable e) {
                        log.error("Error while calling Action callback from planner "
                                + e.getMessage(), e);
                    }
                }
            }
        }
    }

    public void actionFailed(ManagementAction action, Throwable e) {
        // activeJobs.remove(action);
        log.error("Job Failed at scheduler " + action.getActionID(), e);
        // failedJobs.add(action);
        e.printStackTrace();
        ErrorManager.error(e);

        if (action instanceof CreateServiceAction) {
            removeFromMap(servicesBeingCreated, (CreateServiceAction) action);

            List<ActionCallback> callbacks = actionCallbackMap.get(((CreateServiceAction) action)
                    .getServiceType());
            if (callbacks != null) {
                for (ActionCallback callback : callbacks) {
                    try {
                        callback.actionFailed(action, e);
                    } catch (Throwable e1) {
                        log.error("Error while calling Action callback from planner "
                                + e.getMessage(), e);
                    }
                }
            }
        }

    }

    private void addToMap(Map<QName, List<ManagementAction>> map, CreateServiceAction action) {
        QName serviceType = action.getServiceType();
        List<ManagementAction> actions;
        synchronized (this) {
            actions = map.get(serviceType);
            if (actions == null) {
                actions = new Vector<ManagementAction>(4);
                map.put(serviceType, actions);
            }
        }
        actions.add(action);
    }

    private void removeFromMap(Map<QName, List<ManagementAction>> map, CreateServiceAction action) {
        QName serviceType = action.getServiceType();
        List<ManagementAction> actions;
        synchronized (this) {
            actions = map.get(serviceType);
            if (actions != null) {
                actions.remove(action);
                if (actions.isEmpty()) {
                    map.remove(serviceType);
                }
            }

        }
    }

    /**
     * This method may return array of endpoints where some end points are null,
     * which means planner does not know there endpoints
     * 
     * @param serviceType
     * @param callback2CallWhenPendingActionFinishes
     * @return
     */

    public List<EndpointDetails> getServiceEndpointsByName(QName serviceType,
            ActionCallback callback2CallWhenPendingActionFinishes) {
        List<EndpointDetails> endpoints = new ArrayList<EndpointDetails>(4);

        List<ManagedResource> list = actionContext.getRuleEngine().queryManagedResources(
                "ResolveServiceDependancy", new String[] { serviceType.toString() });
        if (list != null) {
            for (ManagedResource resource : list) {
                endpoints.add(new EndpointDetails(resource.getManagerAgentURL(), true, true));
            }
        }

        synchronized (this) {
            List<ManagementAction> actions = createdServices.get(serviceType);
            if (actions != null) {
                for (ManagementAction action : actions) {
                    String endpoint = ((CreateServiceAction) action).getNewServiceUrl();
                    endpoints.add(new EndpointDetails(endpoint, true, false));
                }
            }
            actions = servicesBeingCreated.get(serviceType);
            if (actions != null) {
                for (ManagementAction action : actions) {
                    endpoints.add(new EndpointDetails(null, false, false));
                }
                if (callback2CallWhenPendingActionFinishes != null) {
                    // register a callback which will be called when pending
                    // actions are done
                    List<ActionCallback> callbacks = actionCallbackMap.get(serviceType);
                    if (callbacks == null) {
                        callbacks = new Vector<ActionCallback>(4);
                        actionCallbackMap.put(serviceType, callbacks);
                    }
                    callbacks.add(callback2CallWhenPendingActionFinishes);
                }
            }
        }
        return endpoints;
    }

    /**
     * @param serviceType
     * @return return true if an endpoint of give service type exists or being
     *         created
     * @throws HasthiException
     */

    public boolean isEndpointsExistsByName(QName serviceType) throws HasthiException {
        List<ManagedResource> list = actionContext.getRuleEngine().queryManagedResources(
                "ResolveServiceDependancy", new String[] { serviceType.toString() });
        if (list != null && list.size() > 0) {
            for (ManagedResource resource : list) {
                if (FailureDetectionTools.isSocketUp(resource.getManagerAgentURL())) {
                    return true;
                }
            }
        }

        List<ManagementAction> actions = createdServices.get(serviceType);
        if (actions != null && actions.size() > 0) {
            return true;
        }
        actions = servicesBeingCreated.get(serviceType);
        if (actions != null && actions.size() > 0) {
            return true;
        }
        return false;
    }

    /**
     * Given a service type, this method returns all service types this service
     * depends on. They are decided by parsing service profiles.
     * 
     * @param serviceType
     * @return
     * @throws HasthiException
     */
    public List<QName> getDependancyList(QName serviceType) throws HasthiException {
        return dependencyGraph.topologySort(serviceType);
    }

    /**
     * Given a service type, this method returns all missing service types this
     * service depends on. They are decided by parsing service profiles - active
     * services.
     * 
     * @param serviceType
     * @return
     * @throws HasthiException
     */
    public List<QName> getMissingDependancyList(QName serviceType) throws HasthiException {
        List<QName> dependancyList = dependencyGraph.topologySort(serviceType);
        Iterator<QName> dependancyListItr = dependancyList.iterator();
        while (dependancyListItr.hasNext()) {
            if (isEndpointsExistsByName(dependancyListItr.next())) {
                dependancyListItr.remove();
            }
        }
        return dependancyList;
    }

    public class EndpointDetails {
        public String endpoint;
        public boolean hasCreated;
        public boolean hasJoined;

        public EndpointDetails(String endpoint, boolean hasCreated, boolean hasJoined) {
            super();
            this.endpoint = endpoint;
            this.hasCreated = hasCreated;
            this.hasJoined = hasJoined;
        }
    }

}
